昨天我們現在實作了 create
,當然只有 create
是遠遠不夠的,今天我們要把其他的功能也補上,其實大部分都在 file_model.rb
實作過,只是將他改寫為用 SQL 語法來執行而已,今天就來看看其他 ORM 的部分吧
要實作 find
,我們一樣要借助 SQL 的條件語法 WHERE
,大概的用法就像是這樣
SELECT column1, column2, ...
FROM table_name
WHERE condition;
所以要做出 find
method,可以這樣寫
# mavericks/lib/mavericks/sqlite_model.rb
def self.find(id)
row = DB.execute <<-SQL
select #{schema.keys.join ","} from #{table}
where id = #{id};
SQL
data = Hash[schema.keys.zip row[0]]
self.new data
end
我們一樣加在 class method 底下,取出資料後一樣 new 一個物件做回傳
那 all 呢?做法跟 find 很像,差別在於我們把 WHERE
條件語法拿掉,並且回傳的是一個陣列
# mavericks/lib/mavericks/sqlite_model.rb
def self.all
row = DB.execute <<-SQL
select #{schema.keys.join ","} from #{table}
SQL
row.map do |attr|
data = Hash[schema.keys.zip attr]
self.new data
end
end
如果你把程式碼中的 row
印出來看,會看到資料結構長的像是這樣
[[1, "鐵人30", "每天一篇文章"],
[2, "週末聚餐", "跟朋友喝一杯"], ...]
透過 map
搭配昨天提過的 zip
, 來做 key 和 value 的 merge
就跟 file_model.rb
一樣,我們目前還沒有 method 可以取得資料欄位裡面的內容,需要實作出 getter
以及 setter
,這裡我們一樣先用 []
來實作這一塊
# mavericks/lib/mavericks/sqlite_model.rb
def [](name)
@hash[name.to_s]
end
def []=(name, value)
@hash[name.to_s] = value
end
細心的你應該會發現,這時候我們是做 instance method
,而不是 class method
,相信有跟著做過 file_model.rb
的讀者應該不會陌生,接著回到 just_do 修改一下測試的程式碼
# just_do/sqlite_test.rb
require 'sqlite3'
require 'mavericks/sqlite_model'
class Task < Mavericks::Model::SQLite
end
Task.create("title": "鐵人30", "content": "每天一篇文章")
puts "Count: #{Task.count}"
Task.all.each { |task| puts task['title'] }
puts Task.find(1)['content']
接著執行看一下結果
$ bundle exec ruby sqlite_test.rb
如果沒意外的話,應該可以看到 Count: 數量
這個資訊,因為裡面包含一個執行 create
的程式碼,所以每執行一次 sqlite_test.rb
數量就會加1,另外我們也可以透過 Task.all
來找出所有的 Task,並且利用 each
來印出每一個 task
的 title
,最後透過 find
來尋找特定 ID 的資料,最後透過 task['content']
來取得特定欄位的值
在 file_model.rb
裡面,我們沒有實作修改的部分,我們需要能像 Rails 一樣,透過修改物件屬性,搭配 save
來修改或新增資料,例如像是
# 修改
task['title'] = 'IT邦幫忙鐵人賽'
task.save
# 或是 new 一個 Object 來新增資料
task = Task.new('title': '鐵人30', 'content': '每天一篇文章')
task.save!
接下來就來實作吧!
# mavericks/lib/mavericks/sqlite_model.rb
def save!
unless @hash["id"]
# 如果沒有 ID 就當作是要新增新的資料
self.class.create(@hash)
return true
end
fields = @hash.map { |key, value| "#{key}=#{self.class.to_sql(value)}" }.join ","
# 如果帶有 ID,代表要對特定資料做修改
DB.execute <<-SQL
UPDATE #{self.class.table}
SET #{fields}
WHERE id = #{@hash["id"]}
SQL
true
end
def save
self.save! rescue false
end
這裡比較特別的是,我們實作了兩個 method,一個是 save
,另一個是 save!
,寫過 Rails 的人應該對這兩個的差別不陌生,其實就是對應到 Rails 處理 Exception 的方式,如果呼叫的是 save
,就算過程中出錯,我們也會利用 Ruby 的 rescue
來回傳 false,那如果呼叫的 save!
,就會拋出 Exception
另外值得注意的是,前面所提到的,save
代表了兩種意義,,一個是新增資料的 save
,一個是修改資料的 save
,比較直覺的做法是透過檢查 @hash['id']
,來判斷要執行那個動作,也可以用 SQL 語法的角度來思考,如果沒有 ID
,其實也無法針對特定資料做修改的動作,所以我們假設只要不存在 ID,就當作 save
要新增資料
一樣回到 sqlite_test.rb
測試看看結果
# just_do/sqlite_test.rb
require 'sqlite3'
require 'mavericks/sqlite_model'
class Task < Mavericks::Model::SQLite
end
task = Task.find(1)
task['title'] = '鐵人40'
task.save
puts Task.find(1)['title']
接著執行看一下結果
$ bundle exec ruby sqlite_test.rb
沒意外的話,最後印出來的應該會是 鐵人40
其實做法都是類似,相信聰明的讀者大大們應該都可以做得出來(其實是懶),我就不再多花篇幅介紹了,那接下來還有什麼好繼續寫的?當然有的!有發現到我們一直用 []
method 來代替 .
的用法來取得物件的 Attribute
嗎?應該是要 task.title
而不是 task['title']
,明天我們就來探討實作上的做法吧!